Skip to content

Cranelift: extend alias analysis to eliminate dead stores#13806

Open
fitzgen wants to merge 1 commit into
bytecodealliance:mainfrom
fitzgen:dead-store-elimination
Open

Cranelift: extend alias analysis to eliminate dead stores#13806
fitzgen wants to merge 1 commit into
bytecodealliance:mainfrom
fitzgen:dead-store-elimination

Conversation

@fitzgen

@fitzgen fitzgen commented Jul 2, 2026

Copy link
Copy Markdown
Member

This commit adds an "observed?" bit to every alias region in the alias analysis's LastStores data. Loads from a region set the bit, as do instructions that act as memory fences, like calls. When we see a store to a region that hasn't been observed, and the store is to the same memory location as the last store to that region, then the last store is dead and can be removed. This integrates with alias analysis's existing single forwards pass that is fused with the egraph pass; it doesn't require additional passes or iterations. However, because it is a single pass and not a fixed point, updates do not cascade, and removing a dead store does not then recompute LastStores and reveal that (for example) the dead store's killer is an idempotent store which could also be removed. See the new check-unset-reset-flag.clif filetest, which roughly reflects what component-model fused adapters do with the MAY_LEAVE flag, for an example of this behavior.

Fixes #4167

This commit adds an "observed?" bit to every alias region in the alias
analysis's `LastStores` data. Loads from a region set the bit, as do
instructions that act as memory fences, like calls. When we see a store to a
region that hasn't been observed, and the store is to the same memory location
as the last store to that region, then the last store is dead and can be
removed. This integrates with alias analysis's existing single forwards pass
that is fused with the egraph pass; it doesn't require additional passes or
iterations. However, because it is a single pass and not a fixed point, updates
do not cascade, and removing a dead store does not then recompute `LastStores`
and reveal that (for example) the dead store's killer is an idempotent store
which could also be removed. See the new `check-unset-reset-flag.clif` filetest,
which roughly reflects what component-model fused adapters do with the
`MAY_LEAVE` flag, for an example of this behavior.

Fixes bytecodealliance#4167
@fitzgen
fitzgen requested review from a team as code owners July 2, 2026 18:52
@fitzgen
fitzgen requested review from alexcrichton and removed request for a team July 2, 2026 18:52
@fitzgen

fitzgen commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

This is a speed up for our compile-time builtins benchmarks, as mentioned here:

increment-each-byte-in-buf/concurrency_support=true/compile-time-builtins-host-buf-api
                        time:   [760.93 µs 763.02 µs 765.31 µs]
                        change: [+0.7952% +1.0940% +1.4500%] (p = 0.00 < 0.05)
                        Change within noise threshold.

increment-random-byte-in-buf/concurrency_support=true/compile-time-builtins-host-buf-api
                        time:   [396.16 ns 397.44 ns 398.85 ns]
                        change: [−12.682% −10.658% −8.7674%] (p = 0.00 < 0.05)
                        Performance has improved.

increment-each-byte-in-buf/concurrency_support=false/compile-time-builtins-host-buf-api
                        time:   [296.47 µs 297.83 µs 299.17 µs]
                        change: [−26.293% −24.048% −21.729%] (p = 0.00 < 0.05)
                        Performance has improved.

increment-random-byte-in-buf/concurrency_support=false/compile-time-builtins-host-buf-api
                        time:   [99.568 ns 99.805 ns 100.05 ns]
                        change: [−25.627% −23.111% −20.554%] (p = 0.00 < 0.05)
                        Performance has improved.

On a PCA-based subset of Sightglass:

  • No statistically significant difference for execution
  • No statistically significant difference for compilation on 15/24 benchmarks
  • Statistically significant compilation slowdowns on 9/24 benchmarks, ranging from 1.0x-1.2x to 1.01x-1.08x
Sightglass results
Finished benchmarking in 00h:12m:58s

compilation :: cycles :: benchmarks/kotlin-richards/kotlin-richards.wasm

  Δ = 14913.90 ± 11273.49 (confidence = 99%)

  main.dylib is 1.01x to 1.08x faster than dse.dylib!

  [301551 351870.08 390568] dse.dylib
  [292431 336956.18 367624] main.dylib

compilation :: cycles :: benchmarks/libsodium/libsodium-sodium_utils2.wasm

  Δ = 11259.46 ± 9154.49 (confidence = 99%)

  main.dylib is 1.01x to 1.08x faster than dse.dylib!

  [241739 274766.10 304721] dse.dylib
  [227971 263506.64 305211] main.dylib

compilation :: cycles :: benchmarks/regex/benchmark.wasm

  Δ = 32952.12 ± 30840.00 (confidence = 99%)

  main.dylib is 1.00x to 1.06x faster than dse.dylib!

  [970289 1035352.48 1438627] dse.dylib
  [943980 1002400.36 1232649] main.dylib

compilation :: cycles :: benchmarks/rust-protobuf/benchmark.wasm

  Δ = 8122.94 ± 7198.78 (confidence = 99%)

  main.dylib is 1.00x to 1.06x faster than dse.dylib!

  [257869 277932.68 324798] dse.dylib
  [247861 269809.74 305491] main.dylib

compilation :: cycles :: benchmarks/spidermonkey/spidermonkey-regex.wasm

  Δ = 313441.64 ± 208711.36 (confidence = 99%)

  main.dylib is 1.01x to 1.04x faster than dse.dylib!

  [12750292 13253845.16 14541050] dse.dylib
  [12534325 12940403.52 14251931] main.dylib

compilation :: cycles :: benchmarks/rust-html-rewriter/benchmark.wasm

  Δ = 18136.88 ± 17507.14 (confidence = 99%)

  main.dylib is 1.00x to 1.04x faster than dse.dylib!

  [963333 1003994.12 1098845] dse.dylib
  [944430 985857.24 1117108] main.dylib

compilation :: cycles :: benchmarks/shootout/shootout-switch.wasm

  Δ = 20047.00 ± 14264.82 (confidence = 99%)

  main.dylib is 1.00x to 1.02x faster than dse.dylib!

  [1520847 1563117.68 1632339] dse.dylib
  [1499991 1543070.68 1599627] main.dylib

compilation :: cycles :: benchmarks/tract-onnx-image-classification/benchmark.wasm

  Δ = 262510.32 ± 164289.72 (confidence = 99%)

  main.dylib is 1.00x to 1.02x faster than dse.dylib!

  [21804152 22232734.92 23028528] dse.dylib
  [21495450 21970224.60 23077832] main.dylib

compilation :: cycles :: benchmarks/hashset/benchmark.wasm

  Δ = 25563.28 ± 18660.26 (confidence = 99%)

  main.dylib is 1.00x to 1.02x faster than dse.dylib!

  [2222646 2281357.80 2374406] dse.dylib
  [2169797 2255794.52 2343737] main.dylib

instantiation :: cycles :: benchmarks/libsodium/libsodium-onetimeauth7.wasm

  No difference in performance.

  [1269 1881.36 5494] dse.dylib
  [1422 1760.06 2264] main.dylib

instantiation :: cycles :: benchmarks/hex-simd/benchmark.wasm

  No difference in performance.

  [1313 1820.08 2677] dse.dylib
  [1472 1917.64 2969] main.dylib

instantiation :: cycles :: benchmarks/rust-html-rewriter/benchmark.wasm

  No difference in performance.

  [2036 2465.42 3994] dse.dylib
  [1909 2585.84 6553] main.dylib

instantiation :: cycles :: benchmarks/libsodium/libsodium-sodium_utils2.wasm

  No difference in performance.

  [1373 1861.58 2697] dse.dylib
  [1456 1777.52 2183] main.dylib

instantiation :: cycles :: benchmarks/cm-online-stats/cm-online-stats.wasm

  No difference in performance.

  [2228 2854.96 4322] dse.dylib
  [2117 2734.44 3505] main.dylib

instantiation :: cycles :: benchmarks/splay/splay.wasm

  No difference in performance.

  [1489 1986.74 2954] dse.dylib
  [1523 1904.84 3232] main.dylib

compilation :: cycles :: benchmarks/tinygo/tinygo-json.wasm

  No difference in performance.

  [1453970 1658897.36 1910534] dse.dylib
  [1343060 1591412.80 1877167] main.dylib

instantiation :: cycles :: benchmarks/spidermonkey/spidermonkey-regex.wasm

  No difference in performance.

  [2817 3216.34 4192] dse.dylib
  [2817 3348.50 5011] main.dylib

instantiation :: cycles :: benchmarks/meshoptimizer/benchmark.wasm

  No difference in performance.

  [1446 1788.92 2349] dse.dylib
  [1531 1846.82 2265] main.dylib

compilation :: cycles :: benchmarks/cm-online-stats/cm-online-stats.wasm

  No difference in performance.

  [64689 74000.16 83595] dse.dylib
  [62536 76361.92 323740] main.dylib

compilation :: cycles :: benchmarks/quicksort/benchmark.wasm

  No difference in performance.

  [196717 223366.08 248556] dse.dylib
  [195536 216784.46 250824] main.dylib

compilation :: cycles :: benchmarks/bz2/benchmark.wasm

  No difference in performance.

  [414079 462718.52 509915] dse.dylib
  [406949 449318.56 511436] main.dylib

instantiation :: cycles :: benchmarks/rust-protobuf/benchmark.wasm

  No difference in performance.

  [1504 1965.40 2699] dse.dylib
  [1515 1913.04 2567] main.dylib

instantiation :: cycles :: benchmarks/blake3-scalar/benchmark.wasm

  No difference in performance.

  [1444 1901.52 2644] dse.dylib
  [1542 1950.04 2659] main.dylib

instantiation :: cycles :: benchmarks/shootout/shootout-switch.wasm

  No difference in performance.

  [1372 1634.40 2208] dse.dylib
  [1332 1675.20 2204] main.dylib

instantiation :: cycles :: benchmarks/shootout/shootout-random.wasm

  No difference in performance.

  [1287 1768.64 2418] dse.dylib
  [1368 1729.40 2601] main.dylib

compilation :: cycles :: benchmarks/sqlite3/sqlite3.wasm

  No difference in performance.

  [2028978 2150582.12 2426111] dse.dylib
  [1992886 2103457.22 2361924] main.dylib

compilation :: cycles :: benchmarks/splay/splay.wasm

  No difference in performance.

  [40752 50227.08 65703] dse.dylib
  [41510 49142.34 80924] main.dylib

instantiation :: cycles :: benchmarks/rust-compression/benchmark.wasm

  No difference in performance.

  [3236 3782.54 4717] dse.dylib
  [3325 3864.02 5487] main.dylib

instantiation :: cycles :: benchmarks/regex/benchmark.wasm

  No difference in performance.

  [2343 2897.28 4140] dse.dylib
  [2321 2840.90 3979] main.dylib

execution :: cycles :: benchmarks/libsodium/libsodium-onetimeauth7.wasm

  No difference in performance.

  [131143 137337.38 150095] dse.dylib
  [131473 140004.64 152778] main.dylib

instantiation :: cycles :: benchmarks/hashset/benchmark.wasm

  No difference in performance.

  [1661 1939.42 2366] dse.dylib
  [1670 1904.18 2322] main.dylib

execution :: cycles :: benchmarks/hashset/benchmark.wasm

  No difference in performance.

  [364284 383346.66 430711] dse.dylib
  [367406 390113.94 437923] main.dylib

instantiation :: cycles :: benchmarks/shootout/shootout-keccak.wasm

  No difference in performance.

  [1356 1682.50 2233] dse.dylib
  [1375 1654.66 2407] main.dylib

execution :: cycles :: benchmarks/quicksort/benchmark.wasm

  No difference in performance.

  [216322 237900.00 258167] dse.dylib
  [214027 234091.34 262504] main.dylib

execution :: cycles :: benchmarks/meshoptimizer/benchmark.wasm

  No difference in performance.

  [75675 79689.82 85614] dse.dylib
  [75515 80746.90 86866] main.dylib

compilation :: cycles :: benchmarks/hex-simd/benchmark.wasm

  No difference in performance.

  [193538 214353.42 266493] dse.dylib
  [188717 211563.14 258833] main.dylib

compilation :: cycles :: benchmarks/shootout/shootout-minicsv.wasm

  No difference in performance.

  [40587 46959.44 55809] dse.dylib
  [41479 46364.46 67529] main.dylib

compilation :: cycles :: benchmarks/shootout/shootout-keccak.wasm

  No difference in performance.

  [387820 421629.22 474935] dse.dylib
  [390590 426937.38 464010] main.dylib

instantiation :: cycles :: benchmarks/libsodium/libsodium-scalarmult_ed25519.wasm

  No difference in performance.

  [1521 2032.48 3011] dse.dylib
  [1483 2008.26 2869] main.dylib

execution :: cycles :: benchmarks/shootout/shootout-keccak.wasm

  No difference in performance.

  [68304 72161.40 76890] dse.dylib
  [69138 72965.48 78470] main.dylib

compilation :: cycles :: benchmarks/rust-compression/benchmark.wasm

  No difference in performance.

  [1142122 1741647.68 2082353] dse.dylib
  [1142814 1760990.44 2013696] main.dylib

instantiation :: cycles :: benchmarks/kotlin-richards/kotlin-richards.wasm

  No difference in performance.

  [3420 4276.56 5723] dse.dylib
  [3516 4322.76 7282] main.dylib

instantiation :: cycles :: benchmarks/tract-onnx-image-classification/benchmark.wasm

  No difference in performance.

  [34874 42264.54 57053] dse.dylib
  [34251 42704.50 63048] main.dylib

execution :: cycles :: benchmarks/rust-compression/benchmark.wasm

  No difference in performance.

  [158340 166201.44 191822] dse.dylib
  [158476 167888.06 177944] main.dylib

instantiation :: cycles :: benchmarks/sqlite3/sqlite3.wasm

  No difference in performance.

  [2010 2319.86 2837] dse.dylib
  [1904 2342.14 3099] main.dylib

compilation :: cycles :: benchmarks/blake3-scalar/benchmark.wasm

  No difference in performance.

  [190639 230120.58 283396] dse.dylib
  [199892 227969.50 303630] main.dylib

execution :: cycles :: benchmarks/kotlin-richards/kotlin-richards.wasm

  No difference in performance.

  [444643 463732.58 482387] dse.dylib
  [449079 468074.72 491842] main.dylib

instantiation :: cycles :: benchmarks/tinygo/tinygo-json.wasm

  No difference in performance.

  [1653 2113.88 3052] dse.dylib
  [1793 2096.16 3172] main.dylib

execution :: cycles :: benchmarks/splay/splay.wasm

  No difference in performance.

  [44587672 45155858.02 47174850] dse.dylib
  [44761966 45434209.28 47298269] main.dylib

compilation :: cycles :: benchmarks/shootout/shootout-random.wasm

  No difference in performance.

  [189936 208566.76 234468] dse.dylib
  [184041 207320.00 234296] main.dylib

execution :: cycles :: benchmarks/libsodium/libsodium-scalarmult_ed25519.wasm

  No difference in performance.

  [63755 68012.82 71741] dse.dylib
  [63893 68418.42 76321] main.dylib

compilation :: cycles :: benchmarks/libsodium/libsodium-onetimeauth7.wasm

  No difference in performance.

  [253475 286339.86 369066] dse.dylib
  [249413 284748.24 320236] main.dylib

execution :: cycles :: benchmarks/shootout/shootout-switch.wasm

  No difference in performance.

  [815544 838446.92 863197] dse.dylib
  [812779 842966.12 874425] main.dylib

instantiation :: cycles :: benchmarks/shootout/shootout-minicsv.wasm

  No difference in performance.

  [1291 1664.46 2279] dse.dylib
  [1336 1656.94 2295] main.dylib

instantiation :: cycles :: benchmarks/bz2/benchmark.wasm

  No difference in performance.

  [1532 1879.10 2385] dse.dylib
  [1454 1870.90 2519] main.dylib

execution :: cycles :: benchmarks/rust-protobuf/benchmark.wasm

  No difference in performance.

  [201520 213823.90 231508] dse.dylib
  [201450 212934.06 243603] main.dylib

execution :: cycles :: benchmarks/cm-online-stats/cm-online-stats.wasm

  No difference in performance.

  [10449954 10705319.54 11151990] dse.dylib
  [10514805 10748090.08 11208289] main.dylib

execution :: cycles :: benchmarks/hex-simd/benchmark.wasm

  No difference in performance.

  [65938 70924.48 98382] dse.dylib
  [64564 70653.40 89504] main.dylib

instantiation :: cycles :: benchmarks/quicksort/benchmark.wasm

  No difference in performance.

  [1474 1800.20 2507] dse.dylib
  [1441 1793.84 2453] main.dylib

execution :: cycles :: benchmarks/bz2/benchmark.wasm

  No difference in performance.

  [110230 117605.04 125873] dse.dylib
  [113169 117967.76 142068] main.dylib

execution :: cycles :: benchmarks/shootout/shootout-minicsv.wasm

  No difference in performance.

  [7148613 7333797.64 7632231] dse.dylib
  [7119934 7354849.84 7645026] main.dylib

execution :: cycles :: benchmarks/regex/benchmark.wasm

  No difference in performance.

  [95889 98818.38 108010] dse.dylib
  [95064 98550.76 102502] main.dylib

execution :: cycles :: benchmarks/blake3-scalar/benchmark.wasm

  No difference in performance.

  [92550 98629.78 108287] dse.dylib
  [92312 98363.70 106838] main.dylib

execution :: cycles :: benchmarks/tract-onnx-image-classification/benchmark.wasm

  No difference in performance.

  [3854443 3930013.78 4086243] dse.dylib
  [3873545 3938115.28 4107499] main.dylib

execution :: cycles :: benchmarks/shootout/shootout-random.wasm

  No difference in performance.

  [3344464 3435485.56 3570093] dse.dylib
  [3324341 3428700.84 3604256] main.dylib

execution :: cycles :: benchmarks/rust-html-rewriter/benchmark.wasm

  No difference in performance.

  [146798 151977.58 157691] dse.dylib
  [147479 151692.08 156738] main.dylib

execution :: cycles :: benchmarks/sqlite3/sqlite3.wasm

  No difference in performance.

  [14218916 14415792.74 15383184] dse.dylib
  [14197184 14434728.14 15175099] main.dylib

compilation :: cycles :: benchmarks/meshoptimizer/benchmark.wasm

  No difference in performance.

  [367176 400341.12 437304] dse.dylib
  [362980 399916.12 432478] main.dylib

compilation :: cycles :: benchmarks/libsodium/libsodium-scalarmult_ed25519.wasm

  No difference in performance.

  [340288 380227.20 526483] dse.dylib
  [339950 380512.48 518440] main.dylib

execution :: cycles :: benchmarks/spidermonkey/spidermonkey-regex.wasm

  No difference in performance.

  [66098 68167.70 71128] dse.dylib
  [66336 68190.04 71532] main.dylib

execution :: cycles :: benchmarks/tinygo/tinygo-json.wasm

  No difference in performance.

  [119820 125794.90 129823] dse.dylib
  [122013 125824.44 136271] main.dylib

I can look into speeding things up a bit more, but we should probably discuss this at the next Cranelift meeting.

@alexcrichton
alexcrichton requested review from cfallin and removed request for a team and alexcrichton July 2, 2026 19:31
@alexcrichton

Copy link
Copy Markdown
Member

I'm going to move this to being reviewed by @cfallin who I know is on PTO right now but I think will have sufficiently more context about this that he'll do a much better job than I

@cfallin

cfallin commented Jul 2, 2026

Copy link
Copy Markdown
Member

(happy to do so; logistical note that I'm out on PTO this week and next, then traveling-for-work the following week, so I can't guarantee I'll get to this before Mon Jul 20)

@github-actions github-actions Bot added the cranelift Issues related to the Cranelift code generator label Jul 2, 2026

@cfallin cfallin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks correct to me -- thanks for the really careful work here!

A few points below but nothing fundamental.


/// Determine whether this opcode behaves as a memory fence, i.e.,
/// prohibits any moving of memory accesses across it.
fn has_memory_fence_semantics(op: Opcode) -> bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason that this moved out of inst_predicates? It seems more natural to me to put it there; it's better not to have a distributed set of opcode sets throughout the code, IMHO, rather than reading through that module when we add a new opcode. (Lack of fallthrough to enforce consideration would be best but that's not practical on predicates like this imho)

Comment thread cranelift/codegen/src/egraph/mod.rs
SkeletonInstSimplification::RemoveDeadStore { dead, killer } => {
assert!(!matches!(cursor.position(), CursorPosition::At(inst) if inst == dead));
// Copy the trap code (if any) from the dead store to its
// killer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor subjective point but although compiler jargon can be pretty... forceful... at times, it might be a little nicer to read to call the "killer" something like shadower or overwriter? :-)

.unwrap();
let flags = flags.with_trap_code(Some(code));
let flags = cursor.func.dfg.mem_flags.insert(flags).unwrap();
*cursor.func.dfg.insts[killer].memflags_mut().unwrap() = flags;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if the shadowing ("killing") store had a conflicting trap code? Do we avoid that by construction in alias analysis by not deciding to merge (we don't actually shadow if the trap-code is different)? Should we assert the trap-code is the same here?

(One way to get that behavior in alias analysis if we don't already is to make the trap-code a field in the last-store tuple in the carried flow-sensitive state)

And actually, if we do match in that way, then I believe we don't need memflags_mut anymore, nor this logic. I believe "trap code must match" should cover all of our desired dead-store cases?

&& inst != last_store
// The last store isn't dead if this
// instruction is a fetch-add or something
// like that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify the comment (because this sounds a little bit like a handwave "too complex" case or something): when we have both can_store() and can_load(), the expected semantics are that the load happens before the store, so the last store is in fact observed (just-in-time, right before processing the store effect). That's why we skip in this case, just as we skip when observed.

@fitzgen

fitzgen commented Jul 20, 2026

Copy link
Copy Markdown
Member Author

Regarding the compilation hit that this PR implies: I have a handful of optimizations that I will start making PRs for that recover that compile time in other areas.

@cfallin

cfallin commented Jul 20, 2026

Copy link
Copy Markdown
Member

One point to track as a followup further improvement, possibly: if we have a mode (enforced by Wasmtime API) that ensures that the Store no longer exposes state after a trap occurs (i.e., disallows reads from memories/tables/globals), we could then set a bit on an alias-region definition stating trap_unobservable, and then we could avoid "observing" (and making live) any trap-unobservable stores. This might be a nice performance improvement wherever one runs a Wasm component in the (frankly most common) case that the driver throws away the store upon trap...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cranelift Issues related to the Cranelift code generator

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cranelift: eliminate dead stores

3 participants